package security

import (
	"context"
	"strings"

	"gitee.com/hp-huiw/my-go-core/config"
	"gitee.com/hp-huiw/my-go-core/util"
)

// Example:
//
//	{
//	    "error": "UNAUTHORIZED",
//	    "message": "ERROR_USER_ACCOUNT_NOT_AUTHORIZED",
//	    "path": "/family-expense/api/role/queryNonSystemLevelRoles",
//	    "status": 401,
//	    "success": false,
//	    "timestamp": 1757396710
//	}
type HandlerErrorResponse struct {
	Error     string `json:"error"`
	Message   any    `json:"message"`
	Path      string `json:"path"`
	Status    int    `json:"status"`
	Success   bool   `json:"success"`
	Timestamp int64  `json:"timestamp"`
}

type UserActiveTokenManager interface {
	IsActiveAccessToken(ctx context.Context, username string, deviceID *string, accessToken string) bool
}

// Checks on access token to see if it is a valid one and does not expire yet.
// Also checks on url access permission.
func CheckAuthorization(ctx context.Context, requestMethod string, requestUri string, authorizationHeader string, userActiveTokenManager UserActiveTokenManager, requestMapQuery RequestMapQuery) bool {
	granted := false

	// FullPath returns a matched route full path.
	// 404 returns an empty string.
	urlFullPath := strings.TrimSpace(requestUri)

	if !util.IsEmptyString(urlFullPath) {
		// Gets authorization from request header.
		// The string should be like below example:
		// Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDU3ODIxNzUsImlhdCI6MTYwNTc3ODU3NSwiaWQiOiJ1c2VyLWlkIiwidXNlcm5hbWUiOiJIdWkiLCJyb2xlcyI6WyJBZG1pbiJdfQ.tKdhYNyeqOS_ITIV_F0sKT6N-NBFjxMkPyXAkzT43WQ
		authorizationHeader := strings.TrimSpace(authorizationHeader)

		// Validates token string to see if it is a valid access token and it does not expire yet.
		claims, valid, token := CheckAuthorizationHeader(authorizationHeader)

		// If token is valid we still need to check if it is still active.
		if valid {
			valid = userActiveTokenManager.IsActiveAccessToken(ctx, claims.Username, claims.DeviceID, token)
		}

		// If user was authenticated but token is no longer valid or user is not granted for current url 401 code will be sent.
		if claims != nil && !valid {
			granted = false
		} else {
			// If token is valid we need to check on url access permission.
			granted = checkUrlOnRequestMap(ctx, requestMethod, urlFullPath, claims, requestMapQuery)
		}
	}

	return granted
}

// Checks on authorization header and tris to extract the claims.
func CheckAuthorizationHeader(authorizationHeader string) (claims *MyClaims, valid bool, token string) {
	if !util.IsEmptyString(authorizationHeader) {
		// Fields splits the string authorizationHeader around each instance of one or more consecutive white space characters.
		fields := strings.Fields(authorizationHeader)

		// Checks the string if it contains bearer and token.
		if len(fields) == 2 {
			// Gets token string.
			token = fields[1]

			// Validates token string to see if it is a valid access token and it does not expire yet.
			claims, valid = ValidateToken(token, config.ConfigLoader.GetAppConfig().Jwt.SigningKey)

			// Returns result.
			return claims, valid, token
		}
	}

	return nil, false, ""
}

// Checks requested url on security config request map.
func checkUrlOnRequestMap(ctx context.Context, requestMethod string, url string, claims *MyClaims, requestMapQuery RequestMapQuery) bool {
	var urlWithoutRoot string
	var configuredUrl string
	granted := false

	// Gets configured web server info.
	server := config.ConfigLoader.GetAppConfig().Server

	// Removes root url.
	// For example, url is /family-expense/api/user/get.
	// Url without root is /user/get.
	urlWithoutRoot = strings.Replace(url, server.RouteRootGroupName+server.RouteApiGroupName, "", 1)

	var tempUrl string
	cachedRequestMaps, err := GetCachedRequestMaps(ctx, requestMapQuery)

	if err != nil {
		return false
	}

	for _, item := range cachedRequestMaps {
		// If http request method is configured and it does not match then just ignore.
		if item.HTTPMethod != nil && !util.IsEmptyString(*item.HTTPMethod) && !strings.EqualFold(strings.ToUpper(requestMethod), strings.ToUpper(*item.HTTPMethod)) {
			continue
		}

		// First of all we need to remove trailing wildcard.
		// For example, item.Url is /user/** or /user/*.
		// configuredUrl is /user.
		configuredUrl = strings.Replace(item.URL, "/**", "", 1)
		configuredUrl = strings.Replace(configuredUrl, "/*", "", 1)

		// Removes the configuredUrl from urlWithoutRoot.
		// Before removing, /user/get.
		// After removing, /get.
		tempUrl = strings.Replace(urlWithoutRoot, configuredUrl, "", 1)

		if tempUrl == "" {
			// If tempUrl is empty that means the url is exactly matched with configuredUrl.
			// Then we need to check on item.ConfigAttribute.
			granted = checkUrlOnConfigAttribute(claims, item.ConfigAttribute)

			// If we find exactly match we just need to stop.
			break
		}

		if tempUrl == urlWithoutRoot {
			// If configuredUrl is /user/status then tempUrl will be the same as urlWithoutRoot.
			// That means a mismatch. We need to check on next.
			continue
		}

		if !strings.HasPrefix(tempUrl, "/") {
			// If tempUrl does NOT start with "/" that means the parent url does not fully match.
			// For example
			// configuredUrl: /measuringData
			// urlWithoutRoot: /measuringDataSharing/queryValidDetails
			// tempUrl will be Sharing/queryValidDetails
			continue
		}

		// Checks if url starts with tempUrl.
		// For example /authentication is configured but NOT /api/authentication.
		// We only check wildcard config if url ends with tempUrl.
		if !strings.HasPrefix(urlWithoutRoot, tempUrl) {
			if strings.HasSuffix(item.URL, RequestMapUrlDoubleWildcard) {
				// If tempUrl is /get or /status/check it would also match the double wildcard.
				// Then we need to check on item.ConfigAttribute.
				granted = checkUrlOnConfigAttribute(claims, item.ConfigAttribute)
			} else if strings.HasSuffix(item.URL, RequestMapUrlSingleWildcard) {
				// If tempUrl is /get it can match the single wildcard. But /status/check does NOT.
				// Then we need to check on item.ConfigAttribute.
				if strings.Count(tempUrl, "/") == 1 {
					granted = checkUrlOnConfigAttribute(claims, item.ConfigAttribute)
				}
			}
		}
	}

	return granted
}

// Checks requested url access permission on security configAttribute.
// This would check on authentication and authorization.
func checkUrlOnConfigAttribute(claims *MyClaims, configAttribute string) bool {
	granted := false

	if strings.HasPrefix(configAttribute, RequestMapDenyAll) {
		granted = false
	} else if strings.HasPrefix(configAttribute, RequestMapPermitAll) {
		granted = true
	} else if strings.HasPrefix(configAttribute, RequestMapIsAuthenticatedPrefix) && claims != nil {
		granted = true
	} else if (strings.HasPrefix(configAttribute, RequestMapHasRolePrefix) ||
		strings.HasPrefix(configAttribute, RequestMapHasAnyRolePrefix)) && claims != nil {
		granted = ContainsAnyRoleInConfigAttribute(claims.Roles, configAttribute)
	} else if (strings.HasPrefix(configAttribute, RequestMapHasNoRolePrefix) ||
		strings.HasPrefix(configAttribute, RequestMapHasNoAnyRolePrefix)) && claims != nil {
		granted = !ContainsAnyRoleInConfigAttribute(claims.Roles, configAttribute)
	}

	return granted
}
